@heroiclands/package-build 20.7.0 → 21.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/CONTENT.md +134 -44
  3. package/bin/content-build.mjs +37 -5
  4. package/bin/package-build.mjs +77 -0
  5. package/content-config.mjs +59 -1
  6. package/docs/api.md +149 -19
  7. package/docs/commands.md +75 -0
  8. package/docs/configuration.md +37 -10
  9. package/docs/content-format.md +450 -49
  10. package/engine/content-format.mjs +52 -3
  11. package/engine/content-images.mjs +699 -0
  12. package/engine/dependency-bump.mjs +218 -0
  13. package/engine/frontmatter-lint.mjs +89 -2
  14. package/engine/helpers.mjs +81 -142
  15. package/engine/index.mjs +15 -0
  16. package/engine/infobox-registry.mjs +81 -0
  17. package/engine/infobox-render.mjs +381 -0
  18. package/engine/infobox.mjs +963 -0
  19. package/engine/item-registry.mjs +5 -5
  20. package/engine/journals.mjs +22 -1
  21. package/engine/map-notes.mjs +11 -5
  22. package/engine/metadata-index.mjs +5 -0
  23. package/engine/note-vocabulary.mjs +57 -2
  24. package/engine/pathnames.mjs +374 -0
  25. package/engine/pdf-build.mjs +208 -9
  26. package/engine/pdf-render.mjs +461 -21
  27. package/engine/pdf-toc.mjs +77 -5
  28. package/engine/scenes.mjs +2 -1
  29. package/engine/site-build.mjs +106 -7
  30. package/engine/site-index.mjs +93 -4
  31. package/engine/wikilinks.mjs +93 -0
  32. package/hm3/default-item-art.mjs +14 -15
  33. package/hm3/index.mjs +3 -0
  34. package/hm3/infobox.mjs +64 -0
  35. package/package.json +1 -1
  36. package/sohl/default-item-art.mjs +18 -16
  37. package/sohl/index.mjs +3 -0
  38. package/sohl/infobox.mjs +499 -0
  39. package/types/content-config.d.mts +7 -0
  40. package/types/engine/content-format.d.mts +36 -0
  41. package/types/engine/content-images.d.mts +281 -0
  42. package/types/engine/dependency-bump.d.mts +89 -0
  43. package/types/engine/frontmatter-lint.d.mts +23 -0
  44. package/types/engine/helpers.d.mts +30 -72
  45. package/types/engine/index.d.mts +5 -0
  46. package/types/engine/infobox-registry.d.mts +36 -0
  47. package/types/engine/infobox-render.d.mts +87 -0
  48. package/types/engine/infobox.d.mts +443 -0
  49. package/types/engine/item-registry.d.mts +5 -5
  50. package/types/engine/journals.d.mts +9 -1
  51. package/types/engine/note-vocabulary.d.mts +51 -0
  52. package/types/engine/pathnames.d.mts +189 -0
  53. package/types/engine/pdf-build.d.mts +46 -0
  54. package/types/engine/pdf-render.d.mts +99 -1
  55. package/types/engine/pdf-toc.d.mts +10 -5
  56. package/types/engine/site-build.d.mts +11 -3
  57. package/types/engine/site-index.d.mts +35 -3
  58. package/types/engine/wikilinks.d.mts +22 -0
  59. package/types/hm3/default-item-art.d.mts +5 -6
  60. package/types/hm3/index.d.mts +1 -0
  61. package/types/hm3/infobox.d.mts +22 -0
  62. package/types/sohl/index.d.mts +1 -0
  63. package/types/sohl/infobox.d.mts +145 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,164 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 21.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 742861f: **A pathname names the package that owns the file, and every surface derives its
8
+ own address from it.** A note states a file once — `img:`, `data.portrait:`, a
9
+ map's background, overlay, tile textures and ambient sounds, and the address of
10
+ every image in a body — and four surfaces resolve it: a Foundry install, the
11
+ repository's own tree, the website, and the book. The website used to publish
12
+ the address exactly as authored, so a picture that appeared in Foundry and in
13
+ the book 404'd on the page.
14
+
15
+ **What a pathname looks like now**
16
+
17
+ - `images/map.webp` — a file **this** package ships.
18
+ - `sohl/assets/icons/noun/shield.svg` — a file the `sohl` package ships. The
19
+ first segment is the **content package**, never its Foundry id: `thalorna`,
20
+ not `sohl-thalorna`.
21
+ - An absolute URL, a `data:` URI, a `//host/…` or a `/`-rooted path passes
22
+ through untouched on every surface, which is how a note addresses core Foundry
23
+ art or a package outside this constellation.
24
+
25
+ `thalorna` writing `images/map.webp` publishes
26
+ `modules/sohl-thalorna/assets/images/map.webp` in Foundry,
27
+ `https://cdn.heroiclands.org/thalorna/images/map.webp` on the web, and a copy
28
+ staged at `assets/images/map.webp` in the book.
29
+
30
+ **What every consuming repository must change**
31
+
32
+ - **Convert authored pathnames.** `systems/sohl/assets/X` becomes
33
+ `sohl/assets/X`, and `systems/hm3/images/X` becomes `hm3/assets/images/X`.
34
+ A Foundry-spelled pathname is now refused with a located error naming its
35
+ replacement, so nothing converts silently and nothing is missed.
36
+ - **Set `site.assets`** in `package-build.config.yaml` to the host the site
37
+ serves imagery from. A package-owned image on a page with none set is an
38
+ error naming the key.
39
+ - **HM3 serves its pictures from `assets/images/`.** The default art this
40
+ toolchain pairs with each HM3 item type addresses them there.
41
+ - **`DEFAULT_ITEM_ART` holds pathnames, not install paths.** A runtime reading
42
+ the map directly resolves the pathname for itself.
43
+ - **A package addressed by a pathname must be one the build knows** — its own,
44
+ a game system it compiles content for, or a package under `relationships`.
45
+ Where another package's content name differs from its Foundry id, say so with
46
+ `relationships.<kind>[].contentPackage`.
47
+
48
+ `banner:` is unchanged, and still not a pathname: it names a hero image on the
49
+ site's own asset host, reaches no compiled document and no book, and the Hugo
50
+ theme resolves it.
51
+
52
+ ### Minor Changes
53
+
54
+ - 0d83c42: **Beings**
55
+
56
+ - A being note says what kind of being it is — `character` or `creature` — as a
57
+ tag, declared alongside the stations, the place kinds and `draft` that a note's
58
+ tags are already checked against.
59
+ - A being is one or the other. A note carrying both is refused, naming the note
60
+ and the line, because two answers to one question are no answer. A being
61
+ carrying neither is left alone, so a setting part-way through classifying its
62
+ cast still builds.
63
+ - A misspelt kind is caught the way a misspelt `village` already is: a query for
64
+ the characters in a setting does not find `charcter`, and the list it returns
65
+ still looks complete.
66
+ - 841b98b: **`package-build bump` takes a newer first-party release without reformatting
67
+ the lockfile.** Run it with no arguments to move every `@heroiclands/*`
68
+ dependency to its newest published version, or name the packages to move; add
69
+ `--check` to see what would change and write nothing.
70
+
71
+ npm does the resolving, so a version whose dependency set differs from the one
72
+ it replaces is handled as correctly as one that moves three lines — and then
73
+ both `package-lock.json` and `package.json` are restored to the indentation they
74
+ already used. That is the part worth having: every repository consuming this
75
+ toolchain writes its lockfile with four spaces and prettier-ignores it, npm
76
+ rewrites it with two, and a three-line version change arrives as a whole-file
77
+ reformat nobody can review.
78
+ - 5d46b86: **The page**
79
+
80
+ - Every entry now opens a page of its own, under a full-bleed plate carrying the section it belongs to above its name. A wrapped name grows the plate rather than pushing that line off the top of it.
81
+ - A section opens a page of its own too, plated to twice the depth, so the book reads as parts rather than as one continuous run.
82
+ - The body is set in two columns, on cream stock in dark ink with rust rules. A note's `description:` sets as an epigraph under the plate, and the prose opens on a raised capital.
83
+ - A running foot carries the section, an ornament and the folio, so a page found by chance says where it belongs.
84
+ - A page number in the contents now points at the first page of the thing it names.
85
+ - Columns are print's alone. The website keeps one measure.
86
+
87
+ **Tables and wide content**
88
+
89
+ - A table of more than three columns is set across the page instead of crushed into a column: floated over both where it fits a page, and given single-column pages of its own where it does not.
90
+ - A narrower table sets in the column measure and carries its header onto every page and column it spills onto.
91
+
92
+ **What a section can say about itself**
93
+
94
+ - A section may name the banner its plate is drawn over, the line printed above each entry's name, the name the running foot carries, and how many columns its pages are set in. Everything beneath it agrees unless it says otherwise.
95
+ - A section that names no banner still gets its plate, its kicker and its title. A banner that cannot be read is reported.
96
+ - 790d7ab: **An image says how wide it is and where it sits, once, in the note.** A
97
+ markdown image can carry a directive in the curly-attribute convention Pandoc
98
+ and Kramdown use, and the book, the website and a Foundry journal page each
99
+ honour it:
100
+
101
+ ```markdown
102
+ ![Brànwâal Dôrgaar](images/beings/branwldrgr-portrait.webp){float: top-left}
103
+
104
+ ![Map of Thalorna](images/maps/thalorna.webp){.full-width}
105
+ ```
106
+
107
+ - **Width is a class**, and the ordinary width carries no marker — `.full-width`
108
+ is the only one there is. One column in the book, the text measure on the
109
+ website and in a journal page.
110
+ - **Position is `float:`**, taking `top-left`, `bottom-left`, `top-right`,
111
+ `bottom-right` or `center`. The website and Foundry wrap text around it; print
112
+ cannot wrap around a shape, so a float there occupies the measure and only the
113
+ top-or-bottom half of a position has an effect.
114
+ - **An image is a block** — it stands alone in its paragraph — and its **alt
115
+ text is the caption**, drawn under the picture on every surface.
116
+ - Both vocabularies are **closed**. `{.fullwidth}`, `{width=800}` and
117
+ `{float: middle}` are refused by `content-build lint`, located by file, line
118
+ and column, and they fail the run: an unrecognised value rendering as the
119
+ ordinary width looks exactly like a directive that worked. Nothing beyond the
120
+ two vocabularies and the address reaches emitted markup.
121
+
122
+ **The book prints pictures.** An image authored in a note is copied into the
123
+ output directory and set at the measure its class names, where it used to reach
124
+ the page as italic alt text. An address naming a file the package does not ship
125
+ prints its caption alone and is reported.
126
+
127
+ **A body image's address follows the `img:` rule** — its first segment says
128
+ which package owns the file — so `images/map.webp` reaches a journal page as
129
+ `modules/<package>/assets/images/map.webp`. The website passes an address
130
+ through as authored; one that has to resolve there is written as a full URL.
131
+
132
+ _Styling the two HTML surfaces is each surface's own: a figure carries
133
+ `note-image`, plus `note-image-full-width` and `note-image-float-…` from the
134
+ vocabularies._
135
+ - 086fbb4: **Infoboxes**
136
+
137
+ - Every entry now opens with a summary panel: the subject's own facts first, then one panel for each game system the note reaches.
138
+ - A gear item's price, weight and durability reach the panel whichever way the note states them, each with its unit on it — 160d, 1.1 lbs.
139
+ - Armour states all four protection aspects, an unrated one reading `0`; a weapon states its strike modes one per line, with `—` where a value was never given.
140
+ - A being's system panel gathers attributes, skills, mystical abilities and equipment, each a section that stays whole when the panel breaks across a page or column.
141
+ - Skills group by the family each skill note declares, and equipment by what the gear is. A projectile states what it hits for.
142
+ - Rows are labelled for a reader rather than named after the field behind them, and the panel says what the note says and nothing else: a value the system would have assumed anyway is left off, and so is a word standing in for an unset one — no more _Potency: Na_, and a strike mode that rolls no damage says so rather than printing a die nobody can roll.
143
+ - A system panel is never a heading over nothing. One reads _Not available_ where the system produces no document for the note; one that produces a document but adds nothing to the profile says so. A system with no such concept draws no panel at all.
144
+ - The panel carries no picture. An image belongs in the note's text, where its position decides what comes after it.
145
+ - On the web and in Foundry each panel is a disclosure a reader can fold away, open to begin with, so several stack without burying the prose. In print they simply flow.
146
+ - What a panel holds is settled once and drawn the same way in the book, on the website and in a compendium journal — and a field added to a note type appears in all three without further work.
147
+
148
+ **Content format**
149
+
150
+ - The specification now describes the infobox: where it sits in each medium, the rules that hold everywhere, which fields each panel carries, the three things a system panel can say, and the shape a page publishes it in.
151
+
152
+ ### Patch Changes
153
+
154
+ - 841b98b: **A being that declares no `sohl:` block no longer takes the site build down.**
155
+ `content-build site` failed with `unacceptable kind of an object to dump [object
156
+ Undefined]` for any `type: being` note whose front matter carried no `sohl:` key
157
+ at all, and the throw aborted the entire run rather than the one page. A note
158
+ carrying `sohl: null` had always been fine, so the failure only appeared once a
159
+ tree removed the empty key rather than emptying it. Both shapes now publish the
160
+ same page. Fixes HeroicLands/package-build#478.
161
+
3
162
  ## 20.7.0
4
163
 
5
164
  ### Minor Changes
package/CONTENT.md CHANGED
@@ -259,9 +259,9 @@ What follows from the kind:
259
259
  - **`foundryPackage` is not derived.** For every other kind the loader reads it
260
260
  from the adjacent `package.json`; there is no Foundry package here to carry an
261
261
  id, so `foundryPackage` and `assetRoot` are both `null`. A note's `img:`
262
- therefore names the owning package (`systems/…`, `modules/…`) or a URL — a
263
- path this package would have to serve itself is refused, because Foundry
264
- serves no files for a package it does not install.
262
+ therefore names a `/`-rooted path or a URL — a pathname this package would
263
+ have to serve itself is refused, because Foundry serves no files for a
264
+ package it does not install.
265
265
  - **`package-build manifest` refuses**, rather than emitting a `module.json`
266
266
  advertising an installable package with no id, no packs and no compatibility
267
267
  range. So does `content-build package compile`, which would otherwise exit 0
@@ -709,44 +709,57 @@ whitelist without a builder behind it.
709
709
  same `resolveImg` rule as a note's `img:`, so one spelling means one thing
710
710
  wherever it is written.
711
711
 
712
- #### An asset path's first segment says which package owns it
713
-
714
- Every authored asset path — a registry `img:`, a note's `img:`, an actor's
715
- `portrait:` answers "which package holds this file?" in its **first segment**,
716
- and there are exactly three answers:
717
-
718
- | Authored path starts with | Owner | Emitted |
719
- | ------------------------- | --------------------- | -------------------- |
720
- | `systems/` | a separate **system** | unchanged |
721
- | `modules/` | a separate **module** | unchanged |
722
- | anything else | **this package** | `<assetRoot>/<path>` |
723
-
724
- `assetRoot` is derived, never authored: it is
725
- `<packageKind>/<foundryPackage>/assets`, and it is the one place `systems/sohl`
726
- (or `modules/sohl-thalorna`) is ever spelled. The derivation is **conditional on
727
- the kind** a `documentation` package has no asset root, and the third row is
728
- refused there rather than rooted. So `icons/relic.svg` in a module's
729
- registry compiles to `modules/sohl-relics/assets/icons/relic.svg`, and the same
730
- string in the system's compiles to `systems/sohl/assets/icons/relic.svg`. An
731
- already-served `systems/sohl/assets/icons/…` passes through untouched — which is
732
- what lets a module pair a SoHL default with one of its own types.
733
-
734
- **"Anything else" is the rule, not a list of directories.** A package owns its
712
+ #### A pathname names the package that owns the file
713
+
714
+ Every authored pathname — a registry `img:`, a note's `img:`, an actor's
715
+ `data.portrait:`, a map's background, the address of an image in a body
716
+ answers "which package holds this file?" in its **first segment**, when an
717
+ `assets/` follows it. Everything after that `assets/` is the _suffix_, and a
718
+ pathname that does not open `<package>/assets/` belongs to the package being
719
+ compiled, whole.
720
+
721
+ Four surfaces derive an address from that one statement. For a `thalorna` note
722
+ the package Foundry installs as the module `sohl-thalorna` — writing
723
+ `images/map.webp`:
724
+
725
+ | Surface | Address |
726
+ | ----------- | -------------------------------------------------------- |
727
+ | **Foundry** | `modules/sohl-thalorna/assets/images/map.webp` |
728
+ | **Local** | `assets/images/map.webp` |
729
+ | **Web** | `https://cdn.heroiclands.org/thalorna/images/map.webp` |
730
+ | **Book** | `assets/images/map.webp`, staged beside the Typst source |
731
+
732
+ `sohl/assets/icons/relic.svg` names the `sohl` package's file wherever it is
733
+ written, which is what lets a module pair a SoHL default with one of its own
734
+ types. The package's name is not its Foundry id: `thalorna` is the content and
735
+ `sohl-thalorna` the install, and only the Foundry form carries the second.
736
+
737
+ The web host is `site.assets`, and a package-owned image on a page with none
738
+ set is an error naming that key.
739
+
740
+ **The rule is about ownership, not a list of directories.** A package owns its
735
741
  whole `assets/` tree, so a directory this toolchain has never heard of is still
736
742
  that package's: art under `assets/artwork/` is addressed `artwork/deity.webp`
737
- and rooted exactly as `icons/…` and `images/…` are. An address naming no package
738
- at all — an absolute URL, a `data:` URI, a `/`-rooted path — passes through, on
739
- the same rule rather than as an exception.
743
+ and resolved exactly as `icons/…` and `images/…` are. An address naming no
744
+ package at all — an absolute URL, a `data:` URI, a `/`-rooted path — passes
745
+ through on every surface, on the same rule rather than as an exception.
746
+
747
+ **A `systems/…` or `modules/…` pathname is refused**, with a finding naming the
748
+ replacement: it resolves for Foundry and for neither of the other surfaces, and
749
+ deriving anything from it would put a wrong address on two of the three.
750
+ `worlds/` is not such a root — a package may not ship files out of a world, so
751
+ that path gets the ordinary reading and produces a plainly broken one rather
752
+ than a plausible one that 404s unreported.
740
753
 
741
- `worlds/` is deliberately not exempt: a package may not ship art out of a world,
742
- so prefixing such a path produces a plainly broken one rather than a plausible
743
- one that 404s in Foundry unreported.
754
+ A `documentation` package installs nothing, so it has no Foundry form to derive
755
+ and addresses another package's file by a `/`-rooted path or a URL.
744
756
 
745
757
  **`banner:` is a path that does not follow this rule.** It reaches no compiled
746
- document; it is a top-level key the Hugo theme reads, and the theme prefixes a
747
- relative value with `images/` and joins it onto `params.cdnBaseURL`. The two
748
- address different places — `img:` a file Foundry serves, `banner:` a file the
749
- CDN serves so they are stated apart rather than reconciled. See the
758
+ document and no book; it is a top-level key the Hugo theme reads, and the theme
759
+ prefixes a relative value with `images/` and joins it onto
760
+ `params.cdnBaseURL`. The two address different places — a pathname a file a
761
+ package ships, `banner:` a hero image on the site's own asset host — so they are
762
+ stated apart rather than reconciled. See the
750
763
  [content format specification](docs/content-format.md#banner-addresses-the-cdn-not-the-foundry-install).
751
764
 
752
765
  #### "Names no art" and "wants no art" are different
@@ -2436,6 +2449,75 @@ The `WHERE` clause runs against the same content index the SQL tables read, and
2436
2449
  the build owns the `SELECT … FROM notes` — so a filter cannot name a table, and
2437
2450
  cannot reach another package's notes.
2438
2451
 
2452
+ ### How the book is set
2453
+
2454
+ **Every entry opens a page of its own**, under a full-bleed plate carrying a
2455
+ small-caps kicker above the entry's name. A reference book is consulted rather
2456
+ than read through: an entry beginning halfway down a page is harder to find,
2457
+ cannot carry its own running head honestly, and makes a page number in the
2458
+ contents point at the middle of something else. A section opens a page of its
2459
+ own too, plated to twice the depth, so a section reads as a section rather than
2460
+ as the first entry beneath it.
2461
+
2462
+ **The body is set in two columns**, and every other measure follows from that
2463
+ one: an image with no width class is a column wide, the infobox flows in the
2464
+ column measure and breaks between its sections, and a table wider than three
2465
+ columns is set across the page. Two columns are print's answer and print's
2466
+ alone — a scrolling page has no fixed viewport, so the website keeps one
2467
+ measure with a side rail.
2468
+
2469
+ A note's `description:` sets as an epigraph between short rules under the
2470
+ plate, and the first paragraph of the prose opens on a raised capital. Both
2471
+ disappear rather than leave a shell: a note with no description has no
2472
+ epigraph, and a body opening on a link or a number keeps its own first
2473
+ character. The running foot carries the section's name, an ornament and the
2474
+ folio.
2475
+
2476
+ A table of **more than three columns** is given an explicit span rather than
2477
+ left to overflow the measure. Which span depends on how tall it is, and that is
2478
+ measured while the page is laid out: a table that fits a page is floated across
2479
+ both columns at the top of one, and a longer one is set on single-column pages
2480
+ of its own — a float cannot break, and a table taller than the page placed as
2481
+ one piles its rows on top of each other without a word of warning.
2482
+
2483
+ ### What a section declares, its entries inherit
2484
+
2485
+ A section may carry presentation alongside its `contents:`, and everything
2486
+ beneath it agrees unless it says otherwise:
2487
+
2488
+ ```yaml
2489
+ contents:
2490
+ - sectionName: Beings
2491
+ footer: The Bestiary # what the running foot carries
2492
+ page:
2493
+ banner: assets/images/banners/bestiary.webp # the plate's picture
2494
+ kicker: The Bestiary of Thalorna # the line above each entry's name
2495
+ columns: 2 # the measure this section's pages are set in
2496
+ contents:
2497
+ - filter: "type = 'being'"
2498
+ ```
2499
+
2500
+ | Key | Default | What it does |
2501
+ | -------------- | ---------------------- | ----------------------------------------------------- |
2502
+ | `footer` | the section's own name | The name in the running foot. |
2503
+ | `page.banner` | none | A file this repository ships, plated under the title. |
2504
+ | `page.kicker` | the section's trail | The small-caps line above an entry's name. |
2505
+ | `page.columns` | `2` | Columns, from 1 to 4. |
2506
+
2507
+ `page:` is inherited whole: a subsection declaring one of its keys states the
2508
+ others it wants as well.
2509
+
2510
+ **A missing banner is a plate without a picture.** A section plate implies a
2511
+ banner per section, and art arrives later than rendering does — so a section
2512
+ that names none still gets its plate, its kicker and its title, set over the
2513
+ book's ink, and nothing is reported. A banner the build _cannot read_ is a
2514
+ different matter: that is a statement the tree makes and the build cannot
2515
+ honour, so it is a finding.
2516
+
2517
+ `header:` and `infobox:` are part of the format and read by nothing: the
2518
+ running head is a foot in this design, and which infobox a note draws is
2519
+ decided by the note's type.
2520
+
2439
2521
  ### What it is fenced by
2440
2522
 
2441
2523
  **`publish.site` decides whether a book is built, and it is the only switch.**
@@ -2485,14 +2567,22 @@ hand.
2485
2567
 
2486
2568
  ### What the book gets right, and why each matters
2487
2569
 
2488
- | Property | How |
2489
- | ------------------------ | ----------------------------------------------------------------------------- |
2490
- | Searchable | Real text, not page images — a roster nobody can search for a name is no use. |
2491
- | Bookmark outline | Every section and entry is a heading, so a viewer's sidebar is the way in. |
2492
- | Page-numbered contents | `#outline()`, shallower than the bookmarks — 2,500 entries would be 40 pages. |
2493
- | Repeating table headers | `table.header`, so a property table spilling a page keeps its column names. |
2494
- | Internal cross-reference | A wikilink between two notes of the book becomes an internal destination. |
2495
- | External cross-reference | A cross-package link, and a note the book did not select, stay URLs. |
2570
+ | Property | How |
2571
+ | ------------------------ | ------------------------------------------------------------------------------- |
2572
+ | Searchable | Real text, not page images — a roster nobody can search for a name is no use. |
2573
+ | Bookmark outline | Every section and entry is a heading, so a viewer's sidebar is the way in. |
2574
+ | Page-numbered contents | `#outline()`, shallower than the bookmarks — 2,500 entries would be 40 pages. |
2575
+ | Repeating table headers | `table.header`, so a property table spilling a page keeps its column names. |
2576
+ | Internal cross-reference | A wikilink between two notes of the book becomes an internal destination. |
2577
+ | External cross-reference | A cross-package link, and a note the book did not select, stay URLs. |
2578
+ | Illustration | An image authored in the body is staged and set at the measure its class names. |
2579
+
2580
+ An image states its own width and position once, in the note, and the book, the
2581
+ website and a Foundry journal page each honour it — see
2582
+ [Images](docs/content-format.md#images) for the two closed vocabularies. The book
2583
+ copies each picture it prints into the output directory before the compiler
2584
+ runs, because Typst reads nothing above its own root; an address naming a file
2585
+ this package does not ship prints its caption alone and is reported.
2496
2586
 
2497
2587
  `{#anchor}` on a heading becomes an internal destination namespaced by its
2498
2588
  entry, so `[[being-jaslyne#appearance]]` reaches the section and two notes may
@@ -84,6 +84,7 @@ import { lintContentTree } from "../engine/content-lint.mjs";
84
84
  import { lintContentCharset } from "../engine/content-charset.mjs";
85
85
  import { lintContentHtml } from "../engine/content-html.mjs";
86
86
  import { lintContentIcons } from "../engine/content-icons.mjs";
87
+ import { lintContentImages } from "../engine/content-images.mjs";
87
88
  import { declaredSystems, lintFrontmatter, systemBlocksFor } from "../engine/frontmatter-lint.mjs";
88
89
  import { loadContentFormat } from "../engine/content-format.mjs";
89
90
  import {
@@ -962,6 +963,14 @@ function lintCommand() {
962
963
  skipDirectories: config.skipDirectories,
963
964
  });
964
965
 
966
+ // The width and position an image states. Refused rather than
967
+ // reported, because an unrecognised value rendering as the
968
+ // ordinary width looks exactly like a directive that worked —
969
+ // so these are errors and they fail the run.
970
+ const images = lintContentImages(root, {
971
+ skipDirectories: config.skipDirectories,
972
+ });
973
+
965
974
  const findings = [
966
975
  ...addresses.findings,
967
976
  ...frontmatter.findings,
@@ -969,6 +978,7 @@ function lintCommand() {
969
978
  ...charset.findings,
970
979
  ...icons.findings,
971
980
  ...html.findings,
981
+ ...images.findings,
972
982
  ];
973
983
  // Only an **error** fails the run. Every finding was an error
974
984
  // by then, so this changes nothing on its own —
@@ -1418,7 +1428,13 @@ function pdfCommand() {
1418
1428
  compile: argv.compile !== false,
1419
1429
  });
1420
1430
 
1421
- for (const finding of result.findings) emitDiagnostic(finding);
1431
+ // The shared line, not a second copy of it: an **error** fails
1432
+ // the run and a **warning** does not. A picture the book cannot
1433
+ // carry is a warning — the entry still prints, with its caption
1434
+ // where the image would be — and a pathname no surface can
1435
+ // resolve is an error, because the same statement is wrong in
1436
+ // Foundry and on the website too.
1437
+ const errors = reportFindings(result.findings, {});
1422
1438
 
1423
1439
  if (!result.built) {
1424
1440
  // A reason is a deliberate no-op — the fence, an absent
@@ -1442,9 +1458,7 @@ function pdfCommand() {
1442
1458
  );
1443
1459
  log.info(`Typst source: ${result.typ}`);
1444
1460
  if (result.pdf) log.info(`Book: ${result.pdf}`);
1445
- // Findings are reported, never fatal — a filter that selected
1446
- // nothing is worth fixing and is not worth refusing to publish
1447
- // the other two thousand entries over.
1461
+ if (errors) process.exitCode = 1;
1448
1462
  } catch (err) {
1449
1463
  reportFailure(err);
1450
1464
  process.exitCode = 1;
@@ -1565,7 +1579,25 @@ function siteCommand() {
1565
1579
  message: linkFindingMessage(e),
1566
1580
  });
1567
1581
  }
1568
- if (result.tableErrors.length || result.wikiErrors.length) {
1582
+ // An image whose pathname the site cannot resolve, located the
1583
+ // way a wikilink finding is: by searching the note for the
1584
+ // literal the resolver was handed. The page is written either
1585
+ // way — a missing picture is visible, and stopping before the
1586
+ // write would hide every other finding in the tree behind one
1587
+ // address.
1588
+ for (const e of result.imageErrors) {
1589
+ emitDiagnostic({
1590
+ file: e.file,
1591
+ ...positionOfLiteral(readRawNote(e.file), e.src, e.occurrence),
1592
+ severity: "error",
1593
+ message: e.message,
1594
+ });
1595
+ }
1596
+ if (
1597
+ result.tableErrors.length ||
1598
+ result.wikiErrors.length ||
1599
+ result.imageErrors.length
1600
+ ) {
1569
1601
  process.exitCode = 1;
1570
1602
  return;
1571
1603
  }
@@ -86,6 +86,7 @@ import { SCHEMA_ARTIFACT_FILE } from "../engine/foreign-catalog.mjs";
86
86
  import { validateLangSource } from "../lang.mjs";
87
87
  import { checkLabelRegistry } from "../labels.mjs";
88
88
  import { lintYaml } from "../engine/yaml-lint.mjs";
89
+ import { bumpDependencies } from "../engine/dependency-bump.mjs";
89
90
  import {
90
91
  analyzeCoverage,
91
92
  collectScriptReferences,
@@ -1132,6 +1133,81 @@ function e2eCommand() {
1132
1133
  };
1133
1134
  }
1134
1135
 
1136
+ /**
1137
+ * `bump [packages..]` — take a newer version of a dependency.
1138
+ *
1139
+ * npm does the resolving, so a bump whose dependency set changed is as correct
1140
+ * as one that moves three lines. What this adds is the indentation: every
1141
+ * repository here writes `package-lock.json` with four spaces and
1142
+ * prettier-ignores it, and npm rewrites it with two, so the version change
1143
+ * arrives buried in a whole-file reformat.
1144
+ *
1145
+ * Named nothing, it takes the first-party packages — the ones a person bumps
1146
+ * by hand, the moment a release publishes, usually to unblock the change that
1147
+ * prompted it. Third-party bumps arrive from Dependabot on their own schedule.
1148
+ *
1149
+ * @returns {object} The yargs command module.
1150
+ */
1151
+ function bumpCommand() {
1152
+ return {
1153
+ command: "bump [packages..]",
1154
+ describe: "Take a newer version of a dependency, keeping the lockfile's formatting",
1155
+ builder: (y) =>
1156
+ y
1157
+ .positional("packages", {
1158
+ type: "string",
1159
+ array: true,
1160
+ describe: "Packages to bump (default: every @heroiclands/* dependency)",
1161
+ })
1162
+ .option("tag", {
1163
+ type: "string",
1164
+ default: "latest",
1165
+ describe: "Dist-tag to take",
1166
+ })
1167
+ .option("check", {
1168
+ type: "boolean",
1169
+ default: false,
1170
+ describe: "Report what would change and write nothing",
1171
+ }),
1172
+ handler: handler(async (args) => {
1173
+ const rootDir = process.cwd();
1174
+ const result = bumpDependencies({
1175
+ rootDir,
1176
+ packages: args.packages,
1177
+ tag: args.tag,
1178
+ check: args.check,
1179
+ });
1180
+
1181
+ if (result.changes.length === 0) {
1182
+ const named =
1183
+ result.unchanged.length ?
1184
+ result.unchanged.join(", ")
1185
+ : "no first-party dependency";
1186
+ console.log(`Already on the newest ${args.tag}: ${named}`);
1187
+ return;
1188
+ }
1189
+
1190
+ for (const { name, from, to } of result.changes) {
1191
+ console.log(`${name} ${from ?? "—"} → ${to}`);
1192
+ }
1193
+
1194
+ if (result.checked) {
1195
+ console.log(
1196
+ `\nRun without --check to take ${result.changes.length > 1 ? "them" : "it"}.`,
1197
+ );
1198
+ return;
1199
+ }
1200
+
1201
+ for (const file of result.reindented) {
1202
+ console.log(` kept the existing indentation of ${path.basename(file)}`);
1203
+ }
1204
+ console.log(
1205
+ `\nInstall it with \`npm ci\`, which resolves from the lockfile this just moved.`,
1206
+ );
1207
+ }),
1208
+ };
1209
+ }
1210
+
1135
1211
  yargs(hideBin(process.argv))
1136
1212
  .scriptName("package-build")
1137
1213
  .command(cleanCommand())
@@ -1140,6 +1216,7 @@ yargs(hideBin(process.argv))
1140
1216
  .command(schemaCommand())
1141
1217
  .command(langCommand())
1142
1218
  .command(labelsCommand())
1219
+ .command(bumpCommand())
1143
1220
  .command(yamlCommand())
1144
1221
  .command(bundleCommand())
1145
1222
  .command(releaseCommand())